/** * 控件元素页面 */
<template>
  <div>
    <el-row :gutter="10">
      <el-col :span="7">
        <div
          v-loading="imageLoading"
          class="image-body"
          id="screen"
          :style="'height: ' + imageHeight + 'px'"
        >
          <canvas
            id="fgCanvas"
            class="canvas-fg"
            v-bind:style="canvasStyle"
          ></canvas>
          <canvas
            id="bgCanvas"
            class="canvas-bg"
            v-bind:style="canvasStyle"
          ></canvas>
          <span
            class="finger finger-0"
            style="transform: translate3d(200px, 100px, 0px)"
          ></span>
        </div>
      </el-col>
      <el-col :span="10">
        <div class="tree-header">
          <div class="tree-title">控件元素树</div>
          <el-button
            class="tree-add"
            type="text"
            size="mini"
            icon="el-icon-refresh"
            @click="init"
            >刷新控件</el-button
          >
        </div>
        <div class="tree-input">
          <el-input
            size="mini"
            placeholder="输入关键字过滤"
            v-model="filterText"
          />
        </div>
        <div
          v-loading="treeLoading"
          class="tree-body"
          :style="'height: ' + (imageHeight - 78) + 'px'"
        >
          <el-tree
            class="filter-tree"
            :data="treeData"
            :current-node-key="currentNode"
            check-on-click-node
            :props="defaultProps"
            node-key="id"
            ref="tree"
            :filter-node-method="filterNode"
            @node-click="clickNode"
            highlight-current
            :default-expanded-keys="expandedKeys"
          >
            <span class="custom-tree-node" slot-scope="{ node }">
              <span class="tree-label">{{ node.label }}</span>
            </span>
          </el-tree>
        </div>
      </el-col>
      <el-col :span="7">
        <el-table
          size="mini"
          :height="imageHeight - 29"
          :data="propData"
          @selection-change="handleSelectionChange"
        >
          <el-table-column type="selection" :selectable="selectable" width="45">
          </el-table-column>
          <el-table-column prop="prop" label="属性名"> </el-table-column>
          <el-table-column prop="value" label="属性值" min-width="100">
          </el-table-column>
        </el-table>
        <el-button
          size="mini"
          type="text"
          @click="addControl('PROP')"
          icon="el-icon-plus"
          >属性定位元素</el-button
        >
        <el-button
          size="mini"
          type="text"
          @click="addControl('XPATH')"
          icon="el-icon-plus"
          >XPATH定位元素</el-button
        >
      </el-col>
    </el-row>
    <!-- 添加控件弹框 -->
    <control-append
      :controlVisible="controlVisible"
      :controlForm="controlForm"
      @closeDialog="controlVisible = false"
      @submitControlForm="submitControlForm"
    />
  </div>
</template>
<script>
import controlAppend from "../../caseCenter/common/control/controlAppend";
import { ImagePool } from "@/utils/imagepool";
import { b64toBlob } from "@/utils/base64";
import $ from "jquery";
export default {
  name: "ControlView",
  components: {
    controlAppend,
  },
  props: {
    system: String,
    device: Object,
    imageHeight: Number,
    loading: Boolean,
  },
  data() {
    return {
      canvas: {
        bg: null,
        fg: null,
      },
      canvasStyle: {
        opacity: 0.5,
        width: "inherit",
        height: "inherit",
      },
      lastScreenSize: {
        screen: {},
        canvas: {
          width: 1,
          height: 1,
        },
      },
      imageLoading: false,
      treeLoading: false,
      filterText: "",
      treeData: [],
      cursor: {},
      currentNode: null,
      nodeSelected: null,
      nodeHovered: null,
      nodeHoveredList: [],
      originNodes: [],
      originNodeMaps: {},
      defaultProps: {
        children: "children",
        label: "label",
      },
      expandedKeys: [],
      mapAttrCount: {},
      propData: [],
      selections: [],
      xpathExpr: null,
      controlVisible: false,
      controlForm: {
        id: "",
        name: "",
        system: "",
        by: "",
        expression: "XPATH",
        expressionList: [],
        moduleId: "",
        moduleName: "",
        description: "",
      },
      imagePool: null,
      activity: null,
      packageName: null,
    };
  },
  created() {
    this.imagePool = new ImagePool(100);
  },
  watch: {
    loading(val) {
      if (val === true) {
        this.init();
      }
    },
    filterText(val) {
      this.$refs.tree.filter(val);
    },
  },
  mounted: function () {
    this.canvas.bg = document.getElementById("bgCanvas");
    this.canvas.fg = document.getElementById("fgCanvas");
    this.activeMouseControl();
  },
  methods: {
    // 获取数据方法
    init() {
      this.getScreen();
      this.dumpHierarchy();
    },
    dumpHierarchy() {
      this.treeLoading = true;
      this.$axios
        .get(
          this.device.sources.url +
            "/device/hierarchy?serial=" +
            this.device.serial
        )
        .then((ret) => {
          this.packageName = ret.data.data.packageName;
          this.activity = ret.data.data.activity;
          this.drawAllNodeFromSource(ret.data.data.jsonHierarchy);
          this.nodeSelected = null;
          this.treeLoading = false;
        });
    },
    getScreen() {
      this.imageLoading = true;
      this.$axios
        .get(
          this.device.sources.url +
            "/device/screenshot?serial=" +
            this.device.serial
        )
        .then((ret) => {
          var blob = b64toBlob(
            ret.data.data.data,
            "image/" + ret.data.data.type
          );
          this.drawBlobImageToScreen(blob);
          this.imageLoading = false;
        });
    },
    drawAllNodeFromSource(source) {
      let tree = [];
      let that = this;
      if (this.system === "android") {
        source._type = this.packageName;
        source.name = this.activity;
        tree.push(this.sourceToTree(source));
      } else {
        source.forEach(function (s) {
          tree.push(that.sourceToTree(s));
        });
      }
      this.treeData = tree;

      let nodeMaps = (this.originNodeMaps = {});

      function sourceToNodes(source) {
        let node = Object.assign({}, source); //, { children: undefined });
        nodeMaps[node._id] = node;
        let nodes = [node];
        if (source.children) {
          source.children.forEach(function (s) {
            s._parentId = node._id;
            nodes = nodes.concat(sourceToNodes(s));
          });
        }
        return nodes;
      }
      let nodes = [];
      if (this.system === "android") {
        nodes = sourceToNodes(source);
      } else {
        source.forEach(function (s) {
          nodes = nodes.concat(sourceToNodes(s));
        });
      }
      this.originNodes = nodes;
      this.drawAllNode();
      this.canvasStyle.opacity = 1.0;
    },
    drawBlobImageToScreen(blob) {
      // Support jQuery Promise
      var dtd = $.Deferred();
      var bgcanvas = this.canvas.bg,
        fgcanvas = this.canvas.fg,
        self = this,
        ctx = bgcanvas.getContext("2d"),
        URL = window.URL || window.webkitURL,
        BLANK_IMG =
          "",
        img = this.imagePool.next();

      img.onload = function () {
        fgcanvas.width = bgcanvas.width = img.width;
        fgcanvas.height = bgcanvas.height = img.height;

        ctx.drawImage(img, 0, 0, img.width, img.height);
        self.resizeScreen(img);

        // Try to forcefully clean everything to get rid of memory
        // leaks. Note self despite this effort, Chrome will still
        // leak huge amounts of memory when the developer tools are
        // open, probably to save the resources for inspection. When
        // the developer tools are closed no memory is leaked.
        img.onload = img.onerror = null;
        img.src = BLANK_IMG;
        img = null;
        blob = null;

        URL.revokeObjectURL(url);
        url = null;
        dtd.resolve();
      };

      img.onerror = function () {
        // Happily ignore. I suppose this shouldn't happen, but
        // sometimes it does, presumably when we're loading images
        // too quickly.

        // Do the same cleanup here as in onload.
        img.onload = img.onerror = null;
        img.src = BLANK_IMG;
        img = null;
        blob = null;

        URL.revokeObjectURL(url);
        url = null;
        dtd.reject();
      };
      var url = URL.createObjectURL(blob);
      img.src = url;
      return dtd;
    },
    resizeScreen(img) {
      // check if need update
      if (img) {
        if (
          this.lastScreenSize.canvas.width == img.width &&
          this.lastScreenSize.canvas.height == img.height
        ) {
          return;
        }
      } else {
        img = this.lastScreenSize.canvas;
        if (!img) {
          return;
        }
      }
      var screenDiv = document.getElementById("screen");
      this.lastScreenSize = {
        canvas: {
          width: img.width,
          height: img.height,
        },
        screen: {
          width: screenDiv.clientWidth,
          height: screenDiv.clientHeight,
        },
      };
      var canvasRatio = img.width / img.height;
      var screenRatio = screenDiv.clientWidth / screenDiv.clientHeight;
      if (canvasRatio > screenRatio) {
        Object.assign(this.canvasStyle, {
          width: Math.floor(screenDiv.clientWidth) + "px", //'100%',
          height: Math.floor(screenDiv.clientWidth / canvasRatio) + "px", // 'inherit',
        });
      } else {
        Object.assign(this.canvasStyle, {
          width: Math.floor(screenDiv.clientHeight * canvasRatio) + "px", //'inherit',
          height: Math.floor(screenDiv.clientHeight) + "px", //'100%',
        });
      }
    },
    drawRefresh: function () {
      this.drawAllNode();
      if (this.nodeHovered) {
        this.drawNode(this.nodeHovered, "blue");
      }
      if (this.nodeSelected) {
        this.drawNode(this.nodeSelected, "red");
      }
    },
    drawAllNode() {
      var self = this;
      var canvas = self.canvas.fg;
      var ctx = canvas.getContext("2d");
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      self.originNodes.forEach(function (node) {
        // ignore some types
        if (["Layout"].includes(node.type)) {
          return;
        }
        self.drawNode(node, "black", true);
      });
    },
    drawNode: function (node, color, dashed) {
      if (!node || !node.rect) {
        return;
      }
      var x = node.rect.x,
        y = node.rect.y,
        w = node.rect.width,
        h = node.rect.height;
      color = color || "black";
      var ctx = this.canvas.fg.getContext("2d");
      var rectangle = new Path2D();
      rectangle.rect(x, y, w, h);
      if (dashed) {
        ctx.lineWidth = 1;
        ctx.setLineDash([8, 10]);
      } else {
        ctx.lineWidth = 5;
        ctx.setLineDash([]);
      }
      ctx.strokeStyle = color;
      ctx.stroke(rectangle);
    },
    findNodesByPosition(pos) {
      function isInside(node, x, y) {
        if (!node.rect) {
          return false;
        }
        var lx = node.rect.x,
          ly = node.rect.y,
          rx = node.rect.width + lx,
          ry = node.rect.height + ly;
        return lx < x && x < rx && ly < y && y < ry;
      }

      function nodeArea(node) {
        return node.rect.width * node.rect.height;
      }

      let activeNodes = this.originNodes.filter(function (node) {
        if (!isInside(node, pos.x, pos.y)) {
          return false;
        }
        // skip some types
        if (["Layout", "Sprite"].includes(node.type)) {
          return false;
        }
        return true;
      });

      activeNodes.sort((node1, node2) => {
        return nodeArea(node1) - nodeArea(node2);
      });
      return activeNodes;
    },
    sourceToTree(source) {
      var n = {};
      n.id = source._id;
      n.label = source._type;
      if (source.name) {
        n.label += " - " + source.name;
      }
      if (source.resourceId) {
        n.label += " - " + source.resourceId;
      }
      if (source.children) {
        n.children = [];
        let that = this;
        source.children.forEach(
          function (s) {
            n.children.push(that.sourceToTree(s));
          }.bind(this)
        );
      }
      return n;
    },
    // 过滤节点
    filterNode(val, data) {
      if (!val) return true;
      return data.label.indexOf(val) !== -1;
    },
    clickNode(val) {
      // 点击后图片高亮显示被选中元素
      let node = this.originNodeMaps[val.id];
      this.drawAllNode();
      this.drawNode(node, "red");
      // 右侧列表显示被选中元素
      this.getPropData(node);
    },
    handleSelectionChange(rows) {
      this.selections.splice(0, this.selections.length);
      for (let i = 0; i < rows.length; i++) {
        this.selections.push(rows[i]);
      }
    },
    activeMouseControl() {
      var self = this;
      var element = this.canvas.fg;

      var screen = {
        bounds: {},
      };

      function calculateBounds() {
        var el = element;
        screen.bounds.w = el.offsetWidth;
        screen.bounds.h = el.offsetHeight;
        screen.bounds.x = 0;
        screen.bounds.y = 0;

        while (el.offsetParent) {
          screen.bounds.x += el.offsetLeft;
          screen.bounds.y += el.offsetTop;
          el = el.offsetParent;
        }
      }

      function activeFinger(index, x, y, pressure) {
        var scale = 0.5 + pressure;
        $(".finger-" + index)
          .addClass("active")
          .css("transform", "translate3d(" + x + "px," + y + "px,0)");
      }

      function deactiveFinger(index) {
        $(".finger-" + index).removeClass("active");
      }

      function mouseMoveListener(event) {
        var e = event;
        if (e.originalEvent) {
          e = e.originalEvent;
        }
        // Skip secondary click
        if (e.which === 3) {
          return;
        }
        e.preventDefault();

        var pressure = 0.5;
        activeFinger(0, e.pageX, e.pageY, pressure);
        // that.touchMove(0, x / screen.bounds.w, y / screen.bounds.h, pressure);
      }

      function mouseHoverLeaveListener(event) {
        self.nodeHovered = null;
        self.drawRefresh();
      }

      function mouseUpListener(event) {
        var e = event;
        if (e.originalEvent) {
          e = e.originalEvent;
        }
        // Skip secondary click
        if (e.which === 3) {
          return;
        }
        e.preventDefault();

        var pos = coord(e);
        // change precision
        pos.px = Math.floor(pos.px * 1000) / 1000;
        pos.py = Math.floor(pos.py * 1000) / 1000;
        pos.x = Math.floor(pos.px * element.width);
        pos.y = Math.floor(pos.py * element.height);
        self.cursor = pos;

        self.nodeHovered = null;
        markPosition(self.cursor);

        stopMousing();
      }

      function stopMousing() {
        element.removeEventListener("mousemove", mouseMoveListener);
        element.addEventListener("mousemove", mouseHoverListener);
        element.addEventListener("mouseleave", mouseHoverLeaveListener);
        document.removeEventListener("mouseup", mouseUpListener);
        deactiveFinger(0);
      }

      function coord(event) {
        var e = event;
        if (e.originalEvent) {
          e = e.originalEvent;
        }
        calculateBounds();
        var x = e.pageX - screen.bounds.x;
        var y = e.pageY - screen.bounds.y;
        var px = x / screen.bounds.w;
        var py = y / screen.bounds.h;
        return {
          px: px,
          py: py,
          x: Math.floor(px * element.width),
          y: Math.floor(py * element.height),
        };
      }

      function mouseHoverListener(event) {
        var e = event;
        if (e.originalEvent) {
          e = e.originalEvent;
        }
        // Skip secondary click
        if (e.which === 3) {
          return;
        }
        e.preventDefault();
        // startMousing()

        var x = e.pageX - screen.bounds.x;
        var y = e.pageY - screen.bounds.y;
        var pos = coord(event);

        self.nodeHoveredList = self.findNodesByPosition(pos);
        self.nodeHovered = self.nodeHoveredList[0];
        self.drawRefresh();

        if (self.cursor.px) {
          markPosition(self.cursor);
        }
      }

      function mouseDownListener(event) {
        var e = event;
        if (e.originalEvent) {
          e = e.originalEvent;
        }
        // Skip secondary click
        if (e.which === 3) {
          return;
        }
        e.preventDefault();

        let fakePinch = e.altKey;
        calculateBounds();
        // startMousing()

        var x = e.pageX - screen.bounds.x;
        var y = e.pageY - screen.bounds.y;
        var pressure = 0.5;
        activeFinger(0, e.pageX, e.pageY, pressure);

        if (self.nodeHovered) {
          self.nodeSelected = self.nodeHovered;
          self.drawAllNode();
          self.expandedKeys = [self.nodeSelected._id];
          self.currentNode = self.nodeSelected._id;
          self.$refs.tree.setCurrentKey(self.currentNode);
          self.drawNode(self.nodeSelected, "red");
          self.getPropData(self.nodeSelected);
        }

        element.removeEventListener("mouseleave", mouseHoverLeaveListener);
        element.removeEventListener("mousemove", mouseHoverListener);
        element.addEventListener("mousemove", mouseMoveListener);
        document.addEventListener("mouseup", mouseUpListener);
      }

      function markPosition(pos) {
        var ctx = self.canvas.fg.getContext("2d");
        ctx.fillStyle = "#ff0000"; // red
        ctx.beginPath();
        ctx.arc(pos.x, pos.y, 12, 0, 2 * Math.PI);
        ctx.closePath();
        ctx.fill();

        ctx.fillStyle = "#fff"; // white
        ctx.beginPath();
        ctx.arc(pos.x, pos.y, 8, 0, 2 * Math.PI);
        ctx.closePath();
        ctx.fill();
      }

      /* bind listeners */
      element.addEventListener("mousedown", mouseDownListener);
      element.addEventListener("mousemove", mouseHoverListener);
      element.addEventListener("mouseleave", mouseHoverLeaveListener);
    },
    getPropData(node) {
      // 当前元素的xpath定位推荐
      this.xpathExpr = this.elemXPathLite(node);
      let keys = this.filterAttributeKeys(node);
      let data = [
        { prop: "Xpath", value: this.xpathExpr },
        {
          prop: "坐标",
          value: "(" + this.cursor.px + "," + this.cursor.py + ")",
        },
        {
          prop: "className",
          value:
            this.system === "android"
              ? node._type
              : "XCUIElementType" + node._type,
        },
        { prop: "rect", value: JSON.stringify(node.rect) },
      ];
      keys.forEach((k) => {
        data.push({ prop: k, value: node[k] });
      });
      this.propData = data;
    },
    filterAttributeKeys(node) {
      return Object.keys(node).filter((k) => {
        if (["children", "rect"].includes(k)) {
          return false;
        }
        return !k.startsWith("_");
      });
    },
    incrAttrCount(collectionKey, key) {
      if (!this.mapAttrCount.hasOwnProperty(collectionKey)) {
        this.mapAttrCount[collectionKey] = {};
      }
      let count = this.mapAttrCount[collectionKey][key] || 0;
      this.mapAttrCount[collectionKey][key] = count + 1;
    },
    getAttrCount(collectionKey, key) {
      let mapCount = this.mapAttrCount[collectionKey];
      if (!mapCount) {
        return 0;
      }
      return mapCount[key] || 0;
    },
    elemXPathLite(node) {
      // scan nodes
      this.mapAttrCount = {};
      let that = this;
      this.originNodes.forEach((n) => {
        that.incrAttrCount("label", n.label);
        that.incrAttrCount("resourceId", n.resourceId);
        that.incrAttrCount("text", n.text);
        that.incrAttrCount("_type", n._type);
        that.incrAttrCount("description", n.description);
      });

      const array = [];
      while (node && node._parentId) {
        const parent = this.originNodeMaps[node._parentId];
        if (this.getAttrCount("label", node.label) === 1) {
          array.push(`*[@label="${node.label}"]`);
          break;
        } else if (this.getAttrCount("resourceId", node.resourceId) === 1) {
          array.push(`*[@resource-id="${node.resourceId}"]`);
          break;
        } else if (this.getAttrCount("text", node.text) === 1) {
          array.push(`*[@text="${node.text}"]`);
          break;
        } else if (this.getAttrCount("description", node.description) === 1) {
          array.push(`*[@content-desc="${node.description}"]`);
          break;
        } else if (this.getAttrCount("_type", node._type) === 1) {
          array.push(`${node._type}`);
          break;
        } else if (!parent) {
          array.push(`${node._type}`);
        } else {
          let index = 0;
          parent.children.some((n) => {
            if (n._type == node._type) {
              index++;
            }
            return n._id == node._id;
          });
          array.push(`${node._type}[${index}]`);
        }
        node = parent;
      }
      return `//${array.reverse().join("/")}`;
    },
    selectable(row, index) {
      if (
        [
          "label",
          "resourceId",
          "name",
          "text",
          "type",
          "tag",
          "description",
          "className",
        ].includes(row.prop)
      ) {
        return true;
      }
    },
    // 新增控件
    addControl(val) {
      if ((val === "PROP") & (this.selections.length === 0)) {
        this.$message.warning("属性定位请至少选择一条属性");
        return;
      }
      this.controlForm = {
        system: this.system,
        id: "",
        name: "",
        by: val,
        expression: "",
        expressionList: [],
        moduleId: "",
        moduleName: "",
        description: "",
      };
      if (val === "XPATH") {
        this.controlForm.expression = this.xpathExpr;
      } else {
        let elist = [];
        this.selections.forEach((s) => {
          elist.push({ propName: s.prop, propValue: s.value });
        });
        this.controlForm.expressionList = elist;
      }
      this.controlVisible = true;
    },
    // 提交控件
    submitControlForm() {
      this.controlVisible = false;
    },
  },
};
</script>
<style scoped>
.image-body {
  background-color: white;
}

.canvas-fg {
  z-index: 1;
  position: absolute;
  max-width: 100%;
  height: auto;
  margin-top: 2px;
}

.canvas-bg {
  z-index: 0;
  position: absolute;
  max-width: 100%;
  height: auto;
  margin-top: 2px;
}

.finger {
  position: absolute;
  border-style: solid;
  border-radius: 50%;
  border-color: white;
  border-width: 1mm;
  width: 6mm;
  height: 6mm;
  top: -3mm;
  left: -3mm;
  opacity: 0.7;
  pointer-events: none;
  background: red;
  z-index: 200;
  display: none;
}

.tree-header {
  height: 35px;
  padding-right: 5px;
  border-bottom: 1px solid #ebeef5;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.tree-title {
  float: left;
  padding: 6px 12px;
  font-weight: bold;
  font-size: 12px;
}

.tree-input {
  height: 30px;
  padding: 5px 5px;
}

.filter-tree >>> .el-tree-node__children {
  overflow: visible !important;
}

.tree-body {
  overflow: hidden;
  overflow-x: auto;
  overflow-y: auto;
  white-space: nowrap;
}
</style>
